#region License, Terms and Conditions
//
// Prawks - Copyright (c) 2007 Atif Aziz. All rights reserved.
//
// This library is free software; you can redistribute it and/or modify it under
// the terms of the GNU Lesser General Public License as published by the Free
// Software Foundation; either version 2.1 of the License, or (at your option)
// any later version.
//
// This library is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
// FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
// details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this library; if not, write to the Free Software Foundation, Inc.,
// 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 
//
#endregion

namespace Prawks.CodeProvisioning
{
    using System;
    using System.Reflection;
    using System.Reflection.Emit;

    /// <summary>
    /// Allows a method implementation to be invoked logically.
    /// <seealso cref="LogicalMethodImpl"/>
    /// </summary>
    
    public interface ILogicallyInvocable
    {
        /// <summary>
        /// Invokes the method implementation logically.
        /// </summary>
        /// <param name="call">
        /// A <see cref="LogicalCall"/> instance that represents the logical 
        /// invocation taking place.
        /// </param>
        
        void Invoke(LogicalCall call);
    }

    /// <summary>
    /// Provides a base implementation of <see cref="IMethodImpl"/> and
    /// <see cref="IMethodImplCompiler"/> that uses logical invocation.
    /// Derived classes must implement the <see cref="OnInvoke"/> abstract
    /// member.
    /// </summary>
    
    [ Serializable ]
    public abstract class LogicalMethodImpl : IMethodImpl,
        IMethodImplCompiler, ILogicallyInvocable
    {
        private readonly MethodInfo _method;
        
        /// <summary>
        /// Initializes a new instance of the <see cref="LogicalMethodImpl"/> class
        /// for a given method.
        /// </summary>
        /// <param name="method">
        /// The method for which <see cref="LogicalMethodImpl"/> will supply the 
        /// implementation.
        /// </param>
        
        protected LogicalMethodImpl(MethodInfo method)
        {
            if (method == null)
                throw new ArgumentNullException("method");

            _method = method;
        }

        /// <summary>
        /// Gets the method to which the <see cref="LogicalMethodImpl"/> is bound.
        /// </summary>

        public virtual MethodInfo Method
        {
            get { return _method; }
        }

        MethodBuilder IMethodImplCompiler.Compile(TypeBuilder targetType)
        {
            //
            // Validate parameters
            //

            if (targetType == null)
                throw new ArgumentNullException("targetType");

            if (targetType.BaseType != Method.ReflectedType)
            {
                throw new ArgumentOutOfRangeException("targetType", 
                    string.Format("The base type of {0} is {1}, when it is expected to be {2}.", 
                    targetType.FullName, targetType.BaseType.FullName, Method.ReflectedType.FullName));
            }

            //
            // Note that the SpecialName flag is added for superfluous members 
            // like properties.
            //

            MethodAttributes attributes = MethodAttributes.Public | MethodAttributes.Virtual;
            attributes |= _method.Attributes & MethodAttributes.SpecialName;

            return EmitThunk(targetType, _method, attributes);
        }

        void ILogicallyInvocable.Invoke(LogicalCall call)
        {
            OnInvoke(call);
        }

        /// <summary>
        /// Logically invokes the <see cref="Method"/> implementation provided 
        /// by this instance.
        /// </summary>
        /// <param name="call">The logical call stack.</param>
        /// <remarks>
        /// Deriving classes must override <see cref="OnInvoke"/> and supply the 
        /// implementation for <see cref="Method"/>.
        /// </remarks>
        
        protected abstract void OnInvoke(LogicalCall call);

        /// <summary>
        /// Emits a thunk in MSIL that will logically and dynamically invoke the 
        /// implementation for a specific method at runtime.
        /// </summary>
        /// <param name="targetType">
        /// The type on which to define the new method and bake the implementation.
        /// </param>
        /// <param name="prototype">
        /// The method whose implementation is to baked using a thunk.
        /// </param>
        /// <returns>
        /// Returns the newly defined method containing a thunked implementation.
        /// </returns>

        public static MethodBuilder EmitThunk(TypeBuilder targetType, 
            MethodInfo prototype)
        {
            return EmitThunk(targetType, prototype, prototype.Attributes);
        }

        /// <summary>
        /// Emits a thunk in MSIL that will logically and dynamically invoke the 
        /// implementation for a specific method at runtime.
        /// </summary>
        /// <param name="targetType">
        /// The type on which to define the new method and bake the implementation.
        /// </param>
        /// <param name="prototype">
        /// The method whose implementation is to baked using a thunk.
        /// </param>
        /// <param name="attributes">
        /// The attributes of the new method. 
        /// Callers are encouraged to only specify <b>Public</b> combined with <b>Virtual</b>
        /// from <see cref="MethodAttributes"/>. Other 
        /// combinations may produce unwanted results.
        /// </param>
        /// <returns>
        /// Returns the newly defined method containing a thunked implementation.
        /// </returns>

        public static MethodBuilder EmitThunk(TypeBuilder targetType, 
            MethodInfo prototype, MethodAttributes attributes)
        {
            //
            // Validate parameters
            // TODO: Validate acceptable combination of attribute flags?
            //

            if (targetType == null)
                throw new ArgumentNullException("targetType");

            if (prototype == null)
                throw new ArgumentNullException("prototype");

            //
            // Define the new method and its signature as identical to the
            // prototype.
            //

            ParameterInfo[] parameters = prototype.GetParameters();
            
            MethodBuilder newMethod = targetType.DefineMethod(prototype.Name,
                attributes, prototype.ReturnType, 
                TypeCollection.FromParameters(parameters));

            for (int i = 0; i < parameters.Length; i++)
            {
                newMethod.DefineParameter(i + 1, 
                    parameters[i].IsOut ? ParameterAttributes.Out : ParameterAttributes.None, 
                    parameters[i].Name);
            }

            //
            // Define the method body in IL ... starting with locals.
            //

            ILGenerator il = newMethod.GetILGenerator();

            il.DeclareLocal(typeof(LogicalCall)); // call : LogicalCall
            il.DeclareLocal(typeof(object[])); // paramValues : object[]

            //
            // Allocate an array of objects to hold the parameter values.
            //

            if (parameters.Length > 0)
            {
                il.Emit(OpCodes.Ldc_I4, parameters.Length);
                il.Emit(OpCodes.Newarr, typeof(object));
                il.Emit(OpCodes.Stloc_1); // paramValues : object[]
            }

            //
            // Load each input and input/output parameter name and value 
            // into the respective array. Value types are box as needed.
            //

            for (int i = 0; i < parameters.Length; i++)
            {
                ParameterInfo parameter = parameters[i];

                if (parameter.IsOut)
                    continue;

                il.Emit(OpCodes.Ldloc_1); // paramValues : object[]
                il.Emit(OpCodes.Ldc_I4, i);
                il.Emit(OpCodes.Ldarg, i + 1);

                if (parameter.ParameterType.IsByRef)
                {
                    if (!ILUtility.TryEmitLdind(il, parameter.ParameterType.GetElementType()))
                    {
                        throw new CompilationException(
                            string.Format("{0} in {1} cannot be implemented because the {2} parameter is of the unsupported type {3}.",
                            targetType.BaseType.FullName, prototype.Name,
                            parameter.Name, parameter.ParameterType.FullName));
                    }
                }

                if (parameter.ParameterType.IsValueType)
                    il.Emit(OpCodes.Box, parameter.ParameterType);
            
                il.Emit(OpCodes.Stelem_Ref);
            }

            //
            // Prepare to construct a LogicalCall object ...
            //
            // 1. If the method is static, then push a null for the object 
            //    instance. Otherwise, push this.
            //
            // 2. Push the token of the prototype on the stack. The MethodInfo is 
            //    subsequently obtained by placing a call to  
            //    MethodBase.GetMethodFromHandle().
            //
            // 3. Push the string array of parameter names.
            //
            // 4. Push the object array of parameter values.
            //

            il.Emit((attributes & MethodAttributes.Static) == MethodAttributes.Static ?
                OpCodes.Ldnull : OpCodes.Ldarg_0);

            il.Emit(OpCodes.Ldtoken, prototype);
            il.EmitCall(OpCodes.Call, 
                typeof(MethodBase).GetMethod("GetMethodFromHandle", new Type[] { typeof(RuntimeMethodHandle) }), null);
            il.Emit(OpCodes.Castclass, typeof(MethodInfo));

            il.Emit(OpCodes.Ldloc_1);  // paramNames : string[]

            il.Emit(OpCodes.Newobj, typeof(LogicalCall).GetConstructor(new Type[] { typeof(object), typeof(MethodInfo), typeof(object[]) }));
            il.Emit(OpCodes.Stloc_0); // call : LogicalCall

            //
            // Call LogicalMethodImpl.InvokeImplementation with the 
            // LogicalCall object just constructed.
            //

            il.Emit(OpCodes.Ldloc_0); // call : LogicalCall
            il.EmitCall(OpCodes.Call, typeof(LogicalMethodImpl).GetMethod("InvokeImplementation"), null);

            //
            // After the implementation has been invoked, use 
            // LogicalCall.GetParameterValue to retrieve the 
            // input/output and output parameters value.
            // Process all by-ref parameters...propagating their 
            // values from the parameters collection back to the
            // caller's storage. Value types are unboxed as needed.
            //

            for (int i = 0; i < parameters.Length; i++)
            {
                ParameterInfo parameter = parameters[i];

                if (!parameter.ParameterType.IsByRef)
                    continue;

                il.Emit(OpCodes.Ldarg, i + 1); // +1 needed skip this pointer.

                il.Emit(OpCodes.Ldloc_0); // call : LogicalCall
                il.Emit(OpCodes.Ldc_I4, i);
                il.EmitCall(OpCodes.Call, typeof(LogicalCall).GetMethod("GetParameterValue"), null);

                Type parameterElementType = parameter.ParameterType.GetElementType();

                if (parameterElementType.IsValueType)
                {
                    il.Emit(OpCodes.Unbox, parameterElementType);

                    if (!ILUtility.TryEmitLdind(il, parameterElementType))
                    {
                        throw new CompilationException(
                            string.Format("{0} in {1} cannot be implemented because the {2} parameter is of the unsupported type {3}.",
                            targetType.BaseType.FullName, prototype.Name, 
                            parameter.Name, parameter.ParameterType.FullName));
                    }
                
                    ILUtility.EmitStind(il, parameterElementType);
                }
                else
                {
                    if (parameterElementType != typeof(object))
                        il.Emit(OpCodes.Castclass, parameterElementType);

                    il.Emit(OpCodes.Stind_Ref);
                }
            }

            //
            // Finally, if the return type of the function is not 
            // System.Void, then push the return value. Unbox it or cast it 
            // as needed.
            //

            Type returnType = prototype.ReturnType;

            if (returnType != typeof(void))
            {
                il.Emit(OpCodes.Ldloc_0); // call : LogicalCall
                il.EmitCall(OpCodes.Call, typeof(LogicalCall).GetProperty("ReturnValue").GetGetMethod(), null);

                if (returnType != typeof(object))
                {
                    if (returnType.IsValueType)
                    {
                        il.Emit(OpCodes.Unbox, returnType);

                        if (!ILUtility.TryEmitLdind(il, returnType))
                        {
                            throw new CompilationException(
                                string.Format("{0} in {1} cannot be implemented because its return value is of the unsupported type {2}.",
                                targetType.BaseType.FullName, prototype.Name, returnType.FullName));
                        }
                    }
                    else
                    {
                        il.Emit(OpCodes.Castclass, returnType);
                    }
                }
            }

            il.Emit(OpCodes.Ret);

            return newMethod;
        }

        /// <summary>
        /// Invokes the method that is the target a logical call.
        /// </summary>
        /// <param name="call">
        /// The call state captured at the call site.
        /// </param>
        /// <remarks>
        /// This method is used internally by <see cref="LogicalMethodImpl"/>.
        /// Do not call it.
        /// </remarks>

        public static void InvokeImplementation(LogicalCall call)
        {
            //
            // Validate parameters
            //

            if (call == null) 
                throw new ArgumentNullException("call");

            //
            // Get the implementation of the method that is the target of 
            // the call and then invoke the implementation.
            //

            ILogicallyInvocable impl = MethodImplBinder.GetImplementation(call.Method) as ILogicallyInvocable;

            if (impl == null)
            {
                throw new RuntimeException(
                    string.Format("{0} does not support logical invocation, which requires an implementation of the ILogicallyInvocable interface.",
                    impl.GetType().FullName));
            }

            impl.Invoke(call);
        }
    }

    /// <summary>
    /// Provides a base implementation of <see cref="IPropertyMethodImpl"/> for
    /// property method implementations. This class is for internal use only.
    /// Property method implementation providers must derive from 
    /// either <see cref="LogicalPropertyGetImpl"/> for a property getter method
    /// implementation or <see cref="LogicalPropertyGetImpl"/> for a property setter 
    /// method implementation.
    /// </summary>

    [ Serializable ]
    public abstract class LogicalPropertyImpl : LogicalMethodImpl, IPropertyMethodImpl
    {
        private readonly PropertyInfo _property;

        /// <summary>
        /// Initializes a new instance of the <see cref="LogicalPropertyImpl"/> class
        /// for a given property and its get or set accessor method.
        /// </summary>
        /// <param name="property">
        /// The property to which the implementation is bound.
        /// </param>
        /// <param name="accessorMethod">
        /// The method for which <see cref="LogicalPropertyImpl"/> will supply the 
        /// implementation.
        /// </param>

        internal protected LogicalPropertyImpl(PropertyInfo property, MethodInfo accessorMethod) : 
            base(accessorMethod)
        {
            if (property == null)
                throw new ArgumentNullException("property");

            if (property.GetGetMethod() != accessorMethod &&
                property.GetSetMethod() != accessorMethod)
            {
                throw new ArgumentOutOfRangeException(
                    string.Format("{0} is not an accessor method for the {1} property in {2}.",
                    accessorMethod.Name, property.Name, property.DeclaringType.FullName));
            }

            _property = property;
        }

        /// <summary>
        /// Gets the property to which the implementation is bound.
        /// </summary>

        public virtual PropertyInfo Property
        {
            get { return _property; }
        }
    }

    /// <summary>
    /// Provides a base implementation for property getter method implementations.
    /// </summary>

    [ Serializable ]
    public abstract class LogicalPropertyGetImpl : LogicalPropertyImpl
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="LogicalPropertyGetImpl"/> class
        /// for a given property.
        /// </summary>
        /// <param name="property">
        /// The property for which <see cref="LogicalPropertyGetImpl"/> will supply the 
        /// getter implementation.
        /// </param>

        protected LogicalPropertyGetImpl(PropertyInfo property) : 
            base(property, property.GetGetMethod()) 
        {
            if (property == null)
                throw new ArgumentNullException("property");
        }
    }

    /// <summary>
    /// Provides a base implementation for property setter method implementations.
    /// </summary>

    [ Serializable ]
    public abstract class LogicalPropertySetImpl : LogicalPropertyImpl
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="LogicalPropertySetImpl"/> class
        /// for a given property.
        /// </summary>
        /// <param name="property">
        /// The property for which <see cref="LogicalPropertySetImpl"/> will supply the 
        /// setter implementation.
        /// </param>

        protected LogicalPropertySetImpl(PropertyInfo property) : 
            base(property, property.GetSetMethod()) 
        {
            if (property == null)
                throw new ArgumentNullException("property");
        }
    }
}
